/*
* Copyright (C) 2009 - 2013 Niall 'Rivernile' Scott
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors or contributors be held liable for
* any damages arising from the use of this software.
*
* The aforementioned copyright holder(s) hereby grant you a
* non-transferrable right to use this software for any purpose (including
* commercial applications), and to modify it and redistribute it, subject to
* the following conditions:
*
* 1. This notice may not be removed or altered from any file it appears in.
*
* 2. Any modifications made to this software, except those defined in
* clause 3 of this agreement, must be released under this license, and
* the source code of any modifications must be made available on a
* publically accessible (and locateable) website, or sent to the
* original author of this software.
*
* 3. Software modifications that do not alter the functionality of the
* software but are simply adaptations to a specific environment are
* exempt from clause 2.
*/
package uk.org.rivernile.edinburghbustracker.android.fragments.general;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.text.Html;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.TouchDelegate;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.SimpleExpandableListAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import uk.org.rivernile.android.bustracker.parser.livetimes.Bus;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusParser;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusService;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusStop;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusTimesLoader;
import uk.org.rivernile.android.bustracker.parser.livetimes.BusTimesResult;
import uk.org.rivernile.edinburghbustracker.android.BusStopDatabase;
import uk.org.rivernile.edinburghbustracker.android.PreferencesActivity;
import uk.org.rivernile.edinburghbustracker.android.R;
import uk.org.rivernile.edinburghbustracker.android.SettingsDatabase;
import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs
.DeleteFavouriteDialogFragment;
import uk.org.rivernile.edinburghbustracker.android.livetimes.parser
.EdinburghBus;
import uk.org.rivernile.edinburghbustracker.android.livetimes.parser
.EdinburghBusStop;
import uk.org.rivernile.edinburghbustracker.android.livetimes.parser
.EdinburghParser;
/**
* This fragment shows live bus times. It is perhaps the most important part of
* the application. There are a few things to note;
*
* - This fragment communicates with the BusTimes loader. It is a singleton
* instance which holds the result between rotation changes.
* - There is a progress view, bus times view and error view. This simply
* enables and disables layouts as required.
* - The menu item enabled states change depending on whether bus times are
* being displayed or not.
* - The bus stop name shown is taken from the favourite stops list or the bus
* stop database or finally from the bus tracker service.
*
* @author Niall Scott
*/
public class DisplayStopDataFragment extends Fragment
implements LoaderManager.LoaderCallbacks<BusTimesResult>,
DeleteFavouriteDialogFragment.Callbacks {
private static final int EVENT_REFRESH = 1;
private static final int EVENT_UPDATE_TIME = 2;
private static final String SERVICE_NAME_KEY = "SERVICE_NAME";
private static final String DESTINATION_KEY = "DESTINATION";
private static final String ARRIVAL_TIME_KEY = "ARRIVAL_TIME";
/** This is the stop code argument. */
public static final String ARG_STOPCODE = "stopCode";
/** This is the argument required to force a reload of data. */
public static final String ARG_FORCELOAD = "forceLoad";
private static final String LOADER_ARG_STOPCODES = "stopCodes";
private static final String LOADER_ARG_NUMBER_OF_DEPARTURES =
"numberOfDepartures";
private static final String STATE_KEY_AUTOREFRESH = "autoRefresh";
private static final String STATE_KEY_LAST_REFRESH = "lastRefresh";
private static final String STATE_KEY_EXPANDED_ITEMS = "expandedItems";
private static final int AUTO_REFRESH_PERIOD = 60000;
private static final int LAST_REFRESH_PERIOD = 10000;
private Callbacks callbacks;
private BusStopDatabase bsd;
private SettingsDatabase sd;
private SharedPreferences sp;
private ExpandableListView listView;
private TextView txtLastRefreshed, txtStopName, txtServices, txtError;
private BusTimesExpandableListAdapter listAdapter;
private View layoutTopBar;
private ProgressBar progressSmall, progressBig;
private ImageButton imgbtnFavourite;
private int numDepartures = 4;
private String stopCode;
private String stopName;
private String stopLocality;
private boolean autoRefresh;
private long lastRefresh = 0;
private final ArrayList<String> expandedServices = new ArrayList<String>();
private boolean busTimesLoading = false;
private int hitboxSize;
/**
* Create a new instance of this Fragment, specifying the bus stop code.
*
* @param stopCode The stopCode to load times for.
* @return A new instance of this Fragment.
*/
public static DisplayStopDataFragment newInstance(final String stopCode) {
final DisplayStopDataFragment f = new DisplayStopDataFragment();
final Bundle b = new Bundle();
b.putString(ARG_STOPCODE, stopCode);
f.setArguments(b);
return f;
}
/**
* Create a new instance of this Fragment, specifying the bus stop code and
* if a load of the data should be forced.
*
* @param stopCode The stopCode to load times for.
* @param forceLoad true if data is to be refreshed, false if not.
* @return A new instance of this Fragment.
*/
public static DisplayStopDataFragment newInstance(final String stopCode,
final boolean forceLoad) {
final DisplayStopDataFragment f = new DisplayStopDataFragment();
final Bundle b = new Bundle();
b.putString(ARG_STOPCODE, stopCode);
b.putBoolean(ARG_FORCELOAD, forceLoad);
f.setArguments(b);
return f;
}
/**
* {@inheritDoc}
*/
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
try {
callbacks = (Callbacks) activity;
} catch (ClassCastException e) {
throw new IllegalStateException(activity.getClass().getName() +
" does not implement " + Callbacks.class.getName());
}
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get the various resources we need.
final Context context = getActivity().getApplicationContext();
bsd = BusStopDatabase.getInstance(context);
sd = SettingsDatabase.getInstance(context);
sp = context.getSharedPreferences(PreferencesActivity.PREF_FILE, 0);
hitboxSize = getResources()
.getDimensionPixelOffset(R.dimen.star_hitbox_size);
// Get the stop code from the arguments bundle.
stopCode = getArguments().getString(ARG_STOPCODE);
// Get preferences.
try {
numDepartures = Integer.parseInt(
sp.getString(PreferencesActivity
.PREF_NUMBER_OF_SHOWN_DEPARTURES_PER_SERVICE, "4"));
} catch(NumberFormatException e) {
numDepartures = 4;
}
if(savedInstanceState != null) {
lastRefresh = savedInstanceState.getLong(STATE_KEY_LAST_REFRESH, 0);
autoRefresh = savedInstanceState.getBoolean(STATE_KEY_AUTOREFRESH,
false);
if(savedInstanceState.containsKey(STATE_KEY_EXPANDED_ITEMS)) {
expandedServices.clear();
Collections.addAll(expandedServices,
savedInstanceState.getStringArray(
STATE_KEY_EXPANDED_ITEMS));
}
} else {
autoRefresh = sp.getBoolean(PreferencesActivity.PREF_AUTO_REFRESH,
false);
}
}
/**
* {@inheritDoc}
*/
@Override
public View onCreateView(final LayoutInflater inflater,
final ViewGroup container, final Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.displaystopdata, container,
false);
// Get the UI components we need.
listView = (ExpandableListView)v.findViewById(android.R.id.list);
txtLastRefreshed = (TextView)v.findViewById(R.id.txtLastUpdated);
layoutTopBar = v.findViewById(R.id.layoutTopBar);
txtStopName = (TextView)v.findViewById(R.id.txtStopName);
txtServices = (TextView)v.findViewById(R.id.txtServices);
txtError = (TextView)v.findViewById(R.id.txtError);
progressSmall = (ProgressBar)v.findViewById(R.id.progressSmall);
progressBig = (ProgressBar)v.findViewById(R.id.progressBig);
imgbtnFavourite = (ImageButton)v.findViewById(R.id.imgbtnFavourite);
imgbtnFavourite.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
// Add/remove as favourite.
if(sd.getFavouriteStopExists(stopCode)) {
callbacks.onShowConfirmFavouriteDeletion(stopCode);
} else {
callbacks.onShowAddFavouriteStop(stopCode,
stopLocality != null ?
stopName + ", " + stopLocality : stopName);
}
}
});
// The ListView has a context menu.
registerForContextMenu(listView);
return v;
}
/**
* {@inheritDoc}
*/
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Tell the fragment that there is an options menu.
setHasOptionsMenu(true);
if(stopCode != null && stopCode.length() != 0) {
setStopName();
// Since there is a stop code, there is no reason the bus service
// list cannot be populated.
txtServices.setText(BusStopDatabase.getColouredServiceListString(
bsd.getBusServicesForStopAsString(stopCode)));
if(getArguments().getBoolean(ARG_FORCELOAD, false)) {
loadBusTimes(true);
} else {
loadBusTimes(false);
}
} else {
handleError(BusParser.ERROR_NOCODE);
}
getArguments().remove(ARG_FORCELOAD);
}
/**
* {@inheritDoc}
*/
@Override
public void onResume() {
super.onResume();
// Make sure there are no EVENT_UPDATE_TIME messages in the queue.
mHandler.removeMessages(EVENT_UPDATE_TIME);
// Set it up again.
updateLastRefreshed();
setUpLastUpdated();
if (autoRefresh && !busTimesLoading) {
setUpAutoRefresh();
}
// Refresh the menu.
getActivity().supportInvalidateOptionsMenu();
// Set the favourite ImageButton.
if(sd.getFavouriteStopExists(stopCode)) {
imgbtnFavourite.setImageResource(R.drawable.ic_list_favourite);
imgbtnFavourite.setContentDescription(
getString(R.string.favourite_rem));
} else {
imgbtnFavourite.setImageResource(
R.drawable.ic_list_unfavourite_light);
imgbtnFavourite.setContentDescription(
getString(R.string.favourite_add));
}
}
/**
* {@inheritDoc}
*/
@Override
public void onPause() {
super.onPause();
// Stop the background tasks when we're pasued.
mHandler.removeMessages(EVENT_REFRESH);
mHandler.removeMessages(EVENT_UPDATE_TIME);
}
/**
* {@inheritDoc}
*/
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_KEY_AUTOREFRESH, autoRefresh);
outState.putLong(STATE_KEY_LAST_REFRESH, lastRefresh);
populateExpandedItemsList();
if(!expandedServices.isEmpty()) {
final String[] items = new String[expandedServices.size()];
outState.putStringArray(STATE_KEY_EXPANDED_ITEMS,
expandedServices.toArray(items));
}
}
/**
* {@inheritDoc}
*/
@Override
public void onCreateOptionsMenu(final Menu menu,
final MenuInflater inflater) {
// Inflate the menu.
inflater.inflate(R.menu.displaystopdata_option_menu, menu);
}
/**
* {@inheritDoc}
*/
@Override
public void onPrepareOptionsMenu(final Menu menu) {
super.onPrepareOptionsMenu(menu);
// Get the menu items.
final MenuItem sortItem = menu.findItem(
R.id.displaystopdata_option_menu_sort);
final MenuItem autoRefreshItem = menu.findItem(
R.id.displaystopdata_option_menu_autorefresh);
final MenuItem proxItem = menu.findItem(
R.id.displaystopdata_option_menu_prox);
final MenuItem timeItem = menu.findItem(
R.id.displaystopdata_option_menu_time);
final MenuItem refreshItem = menu.findItem(
R.id.displaystopdata_option_menu_refresh);
// If progress is being shown, disable the refresh button.
if(progressBig.getVisibility() == View.VISIBLE ||
progressSmall.getVisibility() == View.VISIBLE) {
refreshItem.setEnabled(false);
} else {
refreshItem.setEnabled(true);
}
// If there's no bus times, disable all other menu items.
if(listView.getVisibility() == View.VISIBLE) {
sortItem.setEnabled(true);
proxItem.setEnabled(true);
timeItem.setEnabled(true);
} else {
sortItem.setEnabled(false);
proxItem.setEnabled(false);
timeItem.setEnabled(false);
}
// Sort by time or service?
if(sp.getBoolean(PreferencesActivity.PREF_SERVICE_SORTING, false)) {
sortItem.setTitle(R.string.displaystopdata_menu_sort_service);
} else {
sortItem.setTitle(R.string.displaystopdata_menu_sort_times);
}
// Auto-refresh on or off?
if(autoRefresh) {
autoRefreshItem.setTitle(
R.string.displaystopdata_menu_turnautorefreshoff);
} else {
autoRefreshItem.setTitle(
R.string.displaystopdata_menu_turnautorefreshon);
}
// Proximity alert active or not?
if(sd.isActiveProximityAlert(stopCode)) {
proxItem.setTitle(R.string.displaystopdata_menu_prox_rem)
.setIcon(R.drawable.ic_menu_proximityremove);
} else {
proxItem.setTitle(R.string.displaystopdata_menu_prox_add)
.setIcon(R.drawable.ic_menu_proximityadd);
}
// Time alert active or not?
if(sd.isActiveTimeAlert(stopCode)) {
timeItem.setTitle(R.string.displaystopdata_menu_time_rem)
.setIcon(R.drawable.ic_menu_arrivalremove);
} else {
timeItem.setTitle(R.string.displaystopdata_menu_time_add)
.setIcon(R.drawable.ic_menu_arrivaladd);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch(item.getItemId()) {
case R.id.displaystopdata_option_menu_sort:
// Change the sort preference and ask for a data redisplay.
boolean sortByTime = sp.getBoolean(
PreferencesActivity.PREF_SERVICE_SORTING, false);
sortByTime = !sortByTime;
final SharedPreferences.Editor edit = sp.edit();
edit.putBoolean(PreferencesActivity.PREF_SERVICE_SORTING,
sortByTime);
edit.commit();
loadBusTimes(false);
getActivity().supportInvalidateOptionsMenu();
return true;
case R.id.displaystopdata_option_menu_autorefresh:
// Turn auto-refresh on or off.
if(autoRefresh) {
autoRefresh = false;
mHandler.removeMessages(EVENT_REFRESH);
} else {
autoRefresh = true;
setUpAutoRefresh();
}
getActivity().supportInvalidateOptionsMenu();
return true;
case R.id.displaystopdata_option_menu_refresh:
// Ask for a refresh.
mHandler.removeMessages(EVENT_REFRESH);
loadBusTimes(true);
return true;
case R.id.displaystopdata_option_menu_prox:
if(sd.isActiveProximityAlert(stopCode)) {
callbacks.onShowConfirmDeleteProximityAlert();
} else {
// Show the Activity for adding a new proximity alert.
callbacks.onShowAddProximityAlert(stopCode);
}
return true;
case R.id.displaystopdata_option_menu_time:
if(sd.isActiveTimeAlert(stopCode)) {
callbacks.onShowConfirmDeleteTimeAlert();
} else {
// Show the Activity for adding a new time alert.
callbacks.onShowAddTimeAlert(stopCode, null);
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onCreateContextMenu(final ContextMenu menu, final View v,
final ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
// Create the ListView context menu.
final MenuInflater inflater = getActivity().getMenuInflater();
menu.setHeaderTitle(getString(R.string.displaystopdata_context_title));
inflater.inflate(R.menu.displaystopdata_context_menu, menu);
}
/**
* {@inheritDoc}
*/
@Override
public boolean onContextItemSelected(final MenuItem item) {
// Cast the information parameter.
final ExpandableListContextMenuInfo info =
(ExpandableListContextMenuInfo)item.getMenuInfo();
switch(item.getItemId()) {
case R.id.displaystopdata_context_menu_addarrivalalert:
// Get the position where this data lives.
final int position = ExpandableListView
.getPackedPositionGroup(info.packedPosition);
if(listAdapter != null &&
position < listAdapter.getGroupCount()) {
final HashMap<String, String> groupData =
(HashMap<String, String>)listAdapter
.getGroup(position);
// Fire off the Activity.
callbacks.onShowAddTimeAlert(stopCode,
new String[] { groupData.get(SERVICE_NAME_KEY) });
}
return true;
default:
return super.onContextItemSelected(item);
}
}
/**
* {@inheritDoc}
*/
@Override
public Loader<BusTimesResult> onCreateLoader(final int id,
final Bundle args) {
if(args == null) return null;
showProgress();
busTimesLoading = true;
return new BusTimesLoader(getActivity(), new EdinburghParser(),
args.getStringArray(LOADER_ARG_STOPCODES),
args.getInt(LOADER_ARG_NUMBER_OF_DEPARTURES, 4));
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadFinished(final Loader<BusTimesResult> loader,
final BusTimesResult result) {
busTimesLoading = false;
if(result != null && isAdded()) {
lastRefresh = result.getLastRefresh();
if(result.hasError()) {
handleError(result.getError());
} else {
displayData(result.getResult());
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoaderReset(final Loader<BusTimesResult> loader) {
// Nothing to do here.
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(final Message msg) {
if (!isAdded()) {
return;
}
switch(msg.what) {
case EVENT_REFRESH:
// Do a refresh.
loadBusTimes(true);
break;
case EVENT_UPDATE_TIME:
// Update the last update time.
updateLastRefreshed();
setUpLastUpdated();
break;
default:
break;
}
}
};
/**
* Request new bus times.
*/
private void loadBusTimes(final boolean reload) {
mHandler.removeMessages(EVENT_REFRESH);
final Bundle args = new Bundle();
args.putStringArray(LOADER_ARG_STOPCODES, new String[] { stopCode });
args.putInt(LOADER_ARG_NUMBER_OF_DEPARTURES, numDepartures);
if(reload) {
getLoaderManager().restartLoader(0, args, this);
} else {
getLoaderManager().initLoader(0, args, this);
}
}
/**
* Handle errors.
*
* @param errorCode A number attributed to the error.
*/
private void handleError(final int errorCode) {
switch(errorCode) {
case BusParser.ERROR_NOCONNECTION:
txtError.setText(R.string.displaystopdata_err_noconn);
break;
case BusParser.ERROR_CANNOTRESOLVE:
txtError.setText(R.string.displaystopdata_err_noresolv);
break;
case BusParser.ERROR_NOCODE:
txtError.setText(R.string.displaystopdata_err_nocode);
break;
case BusParser.ERROR_PARSEERR:
txtError.setText(R.string.displaystopdata_err_parseerr);
break;
case BusParser.ERROR_NODATA:
txtError.setText(R.string.displaystopdata_err_nodata);
break;
case BusParser.ERROR_URLMISMATCH:
txtError.setText(R.string.displaystopdata_err_urlmismatch);
break;
case EdinburghParser.ERROR_INVALID_APP_KEY:
txtError.setText(R.string
.displaystopdata_err_api_invalid_key);
break;
case EdinburghParser.ERROR_INVALID_PARAMETER:
txtError.setText(R.string
.displaystopdata_err_api_invalid_parameter);
break;
case EdinburghParser.ERROR_PROCESSING_ERROR:
txtError.setText(R.string
.displaystopdata_err_api_processing_error);
break;
case EdinburghParser.ERROR_SYSTEM_MAINTENANCE:
txtError.setText(R.string
.displaystopdata_err_api_system_maintenance);
break;
case EdinburghParser.ERROR_SYSTEM_OVERLOADED:
txtError.setText(R.string
.displaystopdata_err_api_system_overloaded);
break;
default:
txtError.setText(R.string.displaystopdata_err_unknown);
break;
}
showError();
if(autoRefresh) {
setUpAutoRefresh();
}
}
/**
* Show progress indicators. If the ListView is not shown, then replace the
* huge white space with a progress indicator. If the ListView is shown,
* replace the last updated text with new text and a small progress
* indicator.
*/
private void showProgress() {
txtError.setVisibility(View.GONE);
if(listView.getVisibility() == View.GONE) {
layoutTopBar.setVisibility(View.GONE);
progressBig.setVisibility(View.VISIBLE);
} else {
layoutTopBar.setVisibility(View.VISIBLE);
progressBig.setVisibility(View.GONE);
progressSmall.setVisibility(View.VISIBLE);
}
getActivity().supportInvalidateOptionsMenu();
}
/**
* Show the bus times. Ensure progress and error layouts are removed and
* show the top bar and ListView.
*/
private void showTimes() {
progressBig.setVisibility(View.GONE);
txtError.setVisibility(View.GONE);
progressSmall.setVisibility(View.INVISIBLE);
layoutTopBar.setVisibility(View.VISIBLE);
listView.setVisibility(View.VISIBLE);
getActivity().supportInvalidateOptionsMenu();
layoutTopBar.post(new Runnable() {
@Override
public void run() {
final Rect rect = new Rect();
imgbtnFavourite.getHitRect(rect);
// Assume it's a square
final int adjustBy = (int)
((hitboxSize - (rect.bottom - rect.top)) / 2);
if(adjustBy > 0) {
rect.top -= adjustBy;
rect.bottom += adjustBy;
rect.left -= adjustBy;
rect.right += adjustBy;
}
layoutTopBar.setTouchDelegate(new TouchDelegate(rect,
imgbtnFavourite));
}
});
}
/**
* Show errors. Ensure progress and bus times layouts are removed and show
* the error layout.
*/
private void showError() {
layoutTopBar.setVisibility(View.GONE);
listView.setVisibility(View.GONE);
progressBig.setVisibility(View.GONE);
progressSmall.setVisibility(View.GONE);
txtError.setVisibility(View.VISIBLE);
getActivity().supportInvalidateOptionsMenu();
}
/**
* Set the stop name. Firstly, it checks to see if there is a favourite stop
* for this stop code and uses the user-set name. If not, it checks the bus
* stop database and uses that name. Otherwise, it will use empty String
* for it to be replaced later when the times are loaded with the name from
* the bus tracker web service.
*/
private void setStopName() {
if(sd.getFavouriteStopExists(stopCode)) {
stopName = sd.getNameForStop(stopCode);
} else {
stopName = bsd.getNameForBusStop(stopCode);
stopLocality = bsd.getLocalityForStopCode(stopCode);
}
if(stopName == null || stopName.length() == 0) {
txtStopName.setText(stopCode);
stopName = "";
} else {
final String name;
if(stopLocality != null) {
name = getString(R.string.busstop_locality_coloured,
stopName, stopLocality, stopCode);
} else {
name = getString(R.string.busstop_coloured, stopName, stopCode);
}
txtStopName.setText(Html.fromHtml(name));
}
}
/**
* Display the data once loaded in the ListView.
*/
private void displayData(final HashMap<String, BusStop> data) {
if(data == null) {
// There must be no data.
handleError(BusParser.ERROR_NODATA);
return;
}
// Get the data for this stop code.
final EdinburghBusStop busStop = (EdinburghBusStop)data.get(stopCode);
if(busStop == null) {
// There must be no data for this stop code.
handleError(BusParser.ERROR_NODATA);
return;
}
// If this is just a refresh, populate the expanded items list.
if(listAdapter != null) {
populateExpandedItemsList();
}
// If the stopName could not be set earlier, get it now from the web
// service.
if(stopName == null || stopName.length() == 0) {
stopName = busStop.getStopName();
final String name = getString(R.string.busstop_coloured, stopName,
stopCode);
// Show the user the stop name and stop code.
txtStopName.setText(Html.fromHtml(name));
}
// Get the list of services in the user's preferred order.
final ArrayList<BusService> services;
if(sp.getBoolean(PreferencesActivity.PREF_SERVICE_SORTING, false)) {
services = busStop.getSortedByTimeBusServices();
} else {
services = busStop.getBusServices();
}
// Does the user want to show night services?
final boolean showNightServices =
sp.getBoolean(PreferencesActivity.PREF_SHOW_NIGHT_BUSES, true);
// Declare variables before going in to the loop.
final ArrayList<HashMap<String, String>> groupData =
new ArrayList<HashMap<String, String>>();
final ArrayList<ArrayList<HashMap<String, String>>> childData =
new ArrayList<ArrayList<HashMap<String, String>>>();
HashMap<String, String> curGroupMap;
ArrayList<HashMap<String, String>> children;
HashMap<String, String> curChildMap;
EdinburghBus bus;
String timeToDisplay, destination;
int mins;
boolean first;
// Loop through the list of services.
for(BusService busService : services) {
if(!showNightServices &&
busService.getServiceName().startsWith("N")) continue;
curGroupMap = new HashMap<String, String>();
groupData.add(curGroupMap);
// Add the service name.
curGroupMap.put(SERVICE_NAME_KEY, busService.getServiceName());
children = new ArrayList<HashMap<String, String>>();
first = true;
// Loop through the buses inside a service.
for(Bus lBus : busService.getBuses()) {
bus = (EdinburghBus)lBus;
destination = bus.getDestination();
if(bus.isDiverted()) {
// Special case if diverted.
timeToDisplay = "";
// Destination may be null when it comes back from the web
// service. Display diverted notice accordingly.
if(destination != null) {
destination += " (" +
getString(R.string.displaystopdata_diverted) +
')';
} else {
destination = getString(R.string
.displaystopdata_diverted);
}
} else {
// Get the number of minutes until arrival.
mins = bus.getArrivalMinutes();
if(mins > 59) {
// If more than 59 minutes, display the full time.
timeToDisplay = bus.getArrivalTime();
} else if(mins < 2) {
// If the bus is due in less than 2 mins, show as due.
timeToDisplay = "DUE";
} else {
// Otherwise, display the number of minutes until
// arrival.
timeToDisplay = String.valueOf(mins);
}
// If the time is estimated, prefix this to the time shown.
if(bus.isEstimated()) {
timeToDisplay = '*' + timeToDisplay;
}
// If the destination is null, make it the empty string to
// prevent future problems.
if(destination == null) {
destination = "";
}
}
if(first) {
// If this is the first bus for this service, put this entry
// in the group map.
curGroupMap.put(DESTINATION_KEY, destination);
curGroupMap.put(ARRIVAL_TIME_KEY, timeToDisplay);
first = false;
} else {
// Otherwise, put it in the expanded child map.
curChildMap = new HashMap<String, String>();
children.add(curChildMap);
curChildMap.put(DESTINATION_KEY, destination);
curChildMap.put(ARRIVAL_TIME_KEY, timeToDisplay);
}
}
childData.add(children);
}
// Create the adatper. This is ugly.
listAdapter = new BusTimesExpandableListAdapter(
getActivity(), groupData, R.layout.expandable_list_group,
new String[] { SERVICE_NAME_KEY, DESTINATION_KEY,
ARRIVAL_TIME_KEY },
new int[] { R.id.buslist_service, R.id.buslist_destination,
R.id.buslist_time },
childData, R.layout.expandable_list_child,
new String[] { DESTINATION_KEY, ARRIVAL_TIME_KEY },
new int[] { R.id.buschild_destination, R.id.buschild_time });
listView.setAdapter(listAdapter);
final int count = groupData.size();
for(int i = 0; i < count; i++) {
curGroupMap = groupData.get(i);
// Re-expand previously expanded items.
if(expandedServices.contains(curGroupMap.get(SERVICE_NAME_KEY))) {
listView.expandGroup(i);
}
}
showTimes();
if(autoRefresh) setUpAutoRefresh();
updateLastRefreshed();
}
/**
* Update the text that informs the user how long it has been since the bus
* data was last refreshed. This normally gets called about every 10
* seconds.
*/
private void updateLastRefreshed() {
final long timeSinceRefresh = SystemClock.elapsedRealtime() -
lastRefresh;
final int mins = (int)(timeSinceRefresh / 60000);
final String text;
if(lastRefresh <= 0) {
// The data has never been refreshed.
text = getString(R.string.times_never);
} else if(mins > 59) {
// The data was refreshed more than 1 hour ago.
text = getString(R.string.times_greaterthanhour);
} else if(mins == 0) {
// The data was refreshed less than 1 minute ago.
text = getString(R.string.times_lessthanoneminago);
} else {
text = getResources()
.getQuantityString(R.plurals.times_minsago, mins, mins);
}
txtLastRefreshed.setText(getString(R.string.displaystopdata_lastupdated,
text));
}
/**
* Schedule the auto-refresh to execute again 60 seconds after the data was
* last refreshed.
*/
private void setUpAutoRefresh() {
mHandler.removeMessages(EVENT_REFRESH);
final long time = (lastRefresh + AUTO_REFRESH_PERIOD) -
SystemClock.elapsedRealtime();
if(time > 0) {
mHandler.sendEmptyMessageDelayed(EVENT_REFRESH, time);
} else {
mHandler.sendEmptyMessage(EVENT_REFRESH);
}
}
/**
* Schedule the text which denotes the last update time to update in 10
* seconds.
*/
private void setUpLastUpdated() {
mHandler.sendEmptyMessageDelayed(EVENT_UPDATE_TIME,
LAST_REFRESH_PERIOD);
}
/**
* This method populates the ArrayList of expanded list items. It will clear
* the list and loop through the group items in the expanded items to see
* if that item is expanded or not. If the item is expanded, the service
* name will be added to the list.
*/
private void populateExpandedItemsList() {
// Firstly, flush the previous items from the list.
expandedServices.clear();
// The ListAdapter could be null.
if(listAdapter != null) {
// Cache the count.
final int count = listAdapter.getGroupCount();
HashMap<String, String> groupData;
// Loop through all group items.
for(int i = 0; i < count; i++) {
// If the group is expanded, get the service name and add it to
// the list.
if(listView.isGroupExpanded(i)) {
groupData = (HashMap<String, String>)listAdapter
.getGroup(i);
expandedServices.add(groupData.get(SERVICE_NAME_KEY));
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void onConfirmFavouriteDeletion() {
imgbtnFavourite.setImageResource(
R.drawable.ic_list_unfavourite_light);
imgbtnFavourite.setContentDescription(
getString(R.string.favourite_add));
}
/**
* {@inheritDoc}
*/
@Override
public void onCancelFavouriteDeletion() {
// Nothing to do here.
}
/**
* This custom ExpandableListAdapter attributes colours to service names
* in the ExpandableListView.
*/
private static class BusTimesExpandableListAdapter
extends SimpleExpandableListAdapter {
private final Context context;
private final int defaultColour;
private final HashMap<String, String> colours;
/**
* Create a new BusTimesExpandableListAdapter.
*
* @param context A Context instance.
* @param groupData The group data.
* @param groupLayout The layout to use for the group View.
* @param groupFrom An array of keys to use for the group items.
* @param groupTo The TextViews to load the keys in to.
* @param childData The child data.
* @param childLayout The layout to use for the child View.
* @param childFrom An array of keys to use for the child View.
* @param childTo The TextViews to load the keys in to.
*/
public BusTimesExpandableListAdapter(final Context context,
final ArrayList<HashMap<String, String>> groupData,
final int groupLayout, final String[] groupFrom,
final int[] groupTo,
final ArrayList<ArrayList<HashMap<String, String>>> childData,
final int childLayout,
final String[] childFrom, final int[] childTo) {
super(context, groupData, groupLayout, groupFrom, groupTo,
childData, childLayout, childFrom, childTo);
// The superclass has no way to get the context again, so cache it
// here.
this.context = context;
final BusStopDatabase bsd = BusStopDatabase.getInstance(
context.getApplicationContext());
defaultColour = context.getResources().getColor(R.color
.defaultBusColour);
final int size = groupData.size();
// Create an array of String to hold the loaded services.
final String[] services = new String[size];
int i = 0;
// Get the service list from the group data and put it in the
// service array.
for(HashMap<String, String> map : groupData) {
services[i] = map.get(SERVICE_NAME_KEY);
i++;
}
if(size > 0) {
colours = bsd.getServiceColours(services);
} else {
colours = null;
}
}
/**
* {@inheritDoc}
*/
@Override
@SuppressLint({"NewAPI"})
public View getGroupView(final int groupPosition,
final boolean isExpanded, final View convertView,
final ViewGroup parent) {
final View v = super.getGroupView(groupPosition, isExpanded,
convertView, parent);
final TextView txtService = (TextView)v.findViewById(
R.id.buslist_service);
// Get the HashMap for the groupPosition.
final HashMap<String, String> group =
(HashMap<String, String>)getGroup(groupPosition);
// Get the name of the service.
final String service = group.get(SERVICE_NAME_KEY);
// Get the Drawable which makes up the retangle in the background
// with the rounded corners. Make it mutable so it doesn't affect
// other instances of the same Drawable.
final GradientDrawable background;
try {
background = (GradientDrawable)context.getResources()
.getDrawable(R.drawable.bus_service_rounded_background)
.mutate();
} catch(ClassCastException e) {
txtService.setTextColor(Color.BLACK);
return v;
}
// Night services are treated differently to the rest.
if(service.startsWith("N")) {
// Give it a black background.
background.setColor(Color.BLACK);
// We need to replace the text in the TextView because HTML
// formatting has been applied to it, to make the 'N' red.
txtService.setText(
BusStopDatabase.getColouredServiceListString(service));
} else if(colours != null && colours.containsKey(service)) {
try {
// If the colour for the service can be parsed, set the
// background here.
background.setColor(Color.parseColor(
colours.get(service)));
} catch(IllegalArgumentException e) {
// If it cannot be parsed, use the default background
// colour.
background.setColor(defaultColour);
}
} else {
// If not a night service, and a colour doesn't exist for the
// service, use the default colour.
background.setColor(defaultColour);
}
// Set the background and return the View for the group.
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
txtService.setBackground(background);
} else {
txtService.setBackgroundDrawable(background);
}
return v;
}
}
/**
* Any Activities which host this Fragment must implement this interface to
* handle navigation events.
*/
public static interface Callbacks {
/**
* This is called when it should be confirmed with the user that they
* want to delete a favourite bus stop.
*
* @param stopCode The bus stop that the user may want to delete.
*/
public void onShowConfirmFavouriteDeletion(String stopCode);
/**
* This is called when it should be confirmed with the user that they
* want to delete the proximity alert.
*/
public void onShowConfirmDeleteProximityAlert();
/**
* This is called when it should be confirmed with the user that they
* want to delete the time alert.
*/
public void onShowConfirmDeleteTimeAlert();
/**
* This is called when the user wants to add a new favourite bus stop.
*
* @param stopCode The stop code of the bus stop to add.
* @param stopName The default name to use for the bus stop.
*/
public void onShowAddFavouriteStop(String stopCode, String stopName);
/**
* This is called when the user wants to view the interface to add a new
* proximity alert.
*
* @param stopCode The stopCode the proximity alert should be added for.
*/
public void onShowAddProximityAlert(String stopCode);
/**
* This is called when the user wants to view the interface to add a new
* time alert.
*
* @param stopCode The stopCode the time alert should be added for.
* @param defaultServices The services that should be selected by
* default. Set to null if no services should be selected.
*/
public void onShowAddTimeAlert(String stopCode,
String[] defaultServices);
}
}